Category 的加载处理过程
在这篇博客 iOS 程序 main 函数之前发生了什么 有中提到,_objc_init
这个函数是 runtime 系统的初始化函数,于是我们可以直接从 _objc_init
这个函数开始进行分析, Category 加载过程中的函数调用顺序如下:
1 | void _objc_init(void); |
文件名 | 方法 |
---|---|
objc-os.mm | _objc_init |
objc-os.mm | map_images |
objc-os.mm | map_images_nolock |
objc-runtime-new.mm | _read_images |
objc-runtime-new.mm | addUnattachedCategoryForClass |
objc-runtime-new.mm | remethodizeClass |
objc-runtime-new.mm | attachCategories |
objc-runtime-new.mm | attachLists |
_read_images
函数处理当前镜像文件的头部信息,具体步骤:
- 获取镜像文件中的类列表,遍历列表进行类读取(调用
readClass
) - 遍历注册所有的
selector
名字(调用__sel_registerName
) - 遍历读取协议列表(调用
_getObjc2ProtocolList
,readProtocol
) - 遍历读取分类列表(调用
_getObjc2CategoryList
,addUnattachedCategoryForClass
,remethodizeClass
) - 遍历实例化运行时类结构(调用
realizeClass
)
从中可以找到与分类相关的代码:
1 | // Discover categories. |
在上面的代码中,首先通过函数 _getObjc2CategoryList
获取 category
的列表 catlist
,然后遍历 catlist
,获取 category 的 Class,根据 Class(类对象和元类对象) 的实例方法、协议、属性,来判断调用addUnattachedCategoryForClass
函数,并进一步判断是否调用 remethodizeClass
函数。
1 | static void addUnattachedCategoryForClass(category_t *cat, Class cls, |
在 addUnattachedCategoryForClass
函数中通过 unattachedCategories()
函数生成一个单例 MapTable 对象 cats
,从该对象中获取 category 的 list 指针,判断该 list 指针是否为空,分配相应内存空间,最后将 category 的数据插入到单例 MapTable 中。
在 remethodizeClass
函数中将通过 attachCategories
函数,把分类信息附加到相应的类中。attachCategories
函数会将类别中的方法列表,属性和协议列表分别都加入本类中,并假定了类别列表加载的顺序是根据类别文件的加载顺序。
1 | static void |
其中的加载函数为 attachLists
,其关键实现为:
1 | array()->count = newCount; |
在 attachLists
方法主要关注两个变量 array()->lists
和 addedLists
- array()->lists: 类对象原来的方法列表,属性列表,协议列表
- addedLists:传入所有分类的方法列表,属性列表,协议列表
上面代码的作用就是通过 memmove
将原来的类找那个的方法、属性、协议列表分别进行后移,然后通过 memcpy
将传入的方法、属性、协议列表填充到开始的位置。
这里总结一下上面的过程:
1、通过 Runtime 加载某个类的所有 Category 数据
2、把所有 Category 的方法、属性、协议数据,合并到一个大数组中,后面参与编译的 Category 数据,会在数组的前面
3、将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
拓展
load 源码分析
通过 objc4 中的源码进行分析, load
加载过程中的函数调用顺序如下:
1 | void _objc_init(void); |
在 load_images
函数中核心逻辑是调用 prepare_load_methods
与 call_load_methods
。
prepare_load_methods
函数的作用就是提前准备好满足 +load
方法调用条件的类和分类,以供接下来的调用。 然后在这个类中调用了schedule_class_load(Class cls)
方法,并且在入参时对父类递归的调用了,确保父类优先的顺序。
call_load_methods
函数中循环调用所有类的 +load
方法。注意,这里是(调用分类的 +load
方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load);
对 +load
方法进行调用的,而不是使用发送消息 objc_msgSend
的方式。
- 分析
call_load_methods
源码
1 | void call_load_methods(void) |
从中可以看出
1、通过 do-while 循环加载类的 load 方法(call_class_loads
是实现 +load
方法的核心函数);
2、先调用完所有类的 load 方法,再调用分类的 load 方法。
- 分析
call_class_loads
源码
1 | static void call_class_loads(void) |
这个函数的作用就是真正负责调用类的 +load
方法了。它从全局变量 loadable_classes
中取出所有可供调用的类,并进行清零操作,其中 loadable_classes
指向用于保存类信息的内存的首地址,loadable_classes_allocated
标识已分配的内存空间大小,loadable_classes_used
则标识已使用的内存空间大小。然后,循环调用所有类的 +load
方法。注意,这里是(调用分类的 +load
方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load)
; 对 +load
方法进行调用的,而不是使用发送消息 objc_msgSend
的方式。
+ (void)load
小总结
+load
方法会在 runtime 加载类、分类时调用- 每个类、分类的+load,在程序运行过程中只调用一次
- 调用顺序:父类 -> 子类 -> 父类的 category -> 子类的 category
1 | 2018-12-03 15:33:36.915480+0800 CALayerDemo[20979:460542] Foo +[Foo load] |
initialize 源码分析
通过 objc4 中的源码进行分析, initialize
加载过程中的函数调用顺序如下:
1 | Method class_getInstanceMethod(Class cls, SEL sel); |
+ (void)initialize
小总结
+initialize
方法会在类第一次接收到消息时调用- 先调用父类的 +initialize,再调用子类的 +initialize
- 先初始化父类,再初始化子类,每个类只会初始化1次
1 | 2018-12-03 15:33:36.916315+0800 CALayerDemo[20979:460542] Foo(Test) +[Foo(Test) initialize] |
load 与 initialize对比
条件 | +load | +initialize |
---|---|---|
关键方法 | (*load_method)(cls, SEL_load) |
objc_msgSend |
调用时机 | 被添加到 runtime 时 | 收到第一条消息前,可能永远不调用 |
调用顺序 | 父类 -> 子类 -> 父类分类 -> 子类分类 | 父类 -> 子类或(父类分类 -> 子类分类) |
调用次数 | 1次 | 多次 |
是否需要显式调用父类实现 | 否 | 否 |
是否沿用父类的实现 | 否 | 是 |
分类中的实现 | 类和分类都执行 | 覆盖类中的方法,只执行分类的实现 |